#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <ncurses.h>
#include <termios.h>
#include <map>
#include <string>
#include <algorithm>
#include <list>

#include "messages.h"
#include "client_comm.h"

#define eprintf(x...) do { printf(x); ::exit(1); } while (false)

const uint16_t COORDINATOR_PORT = 4000;

std::map<int, std::string> rooms;    // lista de salas e portas
int roomToEnter;                     // porta da sala na qual entrar
char servIp[100];                    // IP do servidor
char username[1000];                 // nome de usuario
char composing[100000];              // mensagem sendo composta enquanto nao e enviada
char charbuffer[100][300];           // buffer de renderizacao da tela
std::list<std::string> messages;     // mensagens no log da tela
bool roomExit;                       // guarda se um pedido de saida de sala ocorreu
int curr = 0, curc = 0;              // linha e coluna atuais para renderizacao
std::string roomExitReason;          // motivo para saida da sala

void roomOperationsLoop();               // Loop de operacoes de sala (criacao, delecao, entrar)
void printRoomList();                    // Requer do coordenador e imprime lista de salas.
void doRoomOperation(bool &exit);        // Pergunta operacao de sala desejada
void doEnterRoomOperation(bool &exit);   // Entra em uma sala
bool isValidRoomName(const char *buf);   // Determina se um nome de sala e valido
void doCreateRoomOperation(bool &exit);  // Cria uma sala
void doDeleteRoomOperation(bool &exit);  // Deleta uma sala
void chatLoop();                         // Loop coorrespondente a um chat aberto e funcionando
void getKeyboardInput(int chatfd);       // Obtem entrada do teclado.
void getPacket(int chatfd);              // Obtem mensagem de chat nova.
void render();                           // Renderiza tela de chat aberto.
void dispatchScreen();                   // Despacha tela renderizada para interface de caracteres.
void discardInputBuffer();               // descarta todo o buffer de entrada.

int main(int argc, char *argv[])
{
	if (argc != 2)
		eprintf("Uso: client <IP do coordenador/servidor>\n");
		
	strcpy(servIp, argv[1]);
	
	try
	{
		for (;;)
		{
			roomOperationsLoop();
			chatLoop();
		}
	}
	catch (int e)
	{ }

	return 0;
}

void roomOperationsLoop()
{
	setbuf(stdout, 0);
	bool exit = false;
	do
	{
		printRoomList();
		doRoomOperation(exit);
	}
	while (!exit);
}

void printRoomList()
{
	// Conexao com o coordenador.
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	
	sockaddr_in serv_addr;
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	inet_pton(AF_INET, servIp, &serv_addr.sin_addr);
	serv_addr.sin_port = htons(COORDINATOR_PORT);
	
	if (connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
		eprintf("Erro ao conectar ao coordenador\n");

	// Envio de mensagem do tipo requisicao de lista de salas.
	{
		MessageLC *msg = new MessageLC;
		msg->type = LC_ROOM_LIST;
		sendMessageLC(sockfd, msg);
		delete msg;
	}

	// Recebimento da lista de salas.
	{
		MessageCL *rawmsg = recvMessageCL(sockfd);
		if (rawmsg == 0 || rawmsg->type != CL_ROOM_LIST)
			eprintf("Erro de comunicacao.");
		MessageCLRoomList *msg = (MessageCLRoomList*)rawmsg;
		printf("Lista de salas (%d salas):\n", msg->room_count);
		rooms.clear();
		for (int i = 0; i < msg->room_count; ++i)
		{
			printf("  [%5d] %s\n", msg->rooms[i].room_port, msg->rooms[i].room_name);
			rooms[msg->rooms[i].room_port] = msg->rooms[i].room_name;
		}
		delete msg;
	}
	
	fflush(stdout);
}

void doRoomOperation(bool &exit)
{
	bool ok;
	do
	{
		printf("Operacao [(1) Entrar, (2) Criar sala, (3) Remover sala, (4) Sair do programa, (5) Refresh lista]: ");
		int op;
		if (scanf(" %d", &op) != 1)
		{
			printf("Operacao invalida.\n");
			discardInputBuffer();
			ok = false;
			break;
		}
		ok = true;
		switch (op)
		{
		case 1:
			doEnterRoomOperation(exit);
			break;
			
		case 2:
			doCreateRoomOperation(exit);
			break;
		
		case 3:
			doDeleteRoomOperation(exit);
			break;
		
		case 4:
			throw 42;
			
		case 5:
			break;
		
		default:
			printf("Operacao invalida.\n");
			ok = false;
			break;
		}
	}
	while (!ok);
}

void doEnterRoomOperation(bool &exit)
{
	bool ok;
	do
	{
		printf("Porta da sala: ");
		int room_port = 0;
		ok = true;
		if (scanf(" %d", &room_port) != 1)
		{
			printf("Porta invalida.\n");
			discardInputBuffer();
			ok = false;
			continue;
		}
		if (room_port == 0 || rooms.find(room_port) == rooms.end())
		{
			printf("Porta invalida.\n");
			ok = false;
			continue;
		}
		roomToEnter = room_port;
	}
	while (!ok);
	
	do
	{
		printf("Nome de usuario (somente alfanum): ");
		ok = true;
		if (scanf(" %s", username) != 1 || strlen(username) == 0 || strlen(username) > 64)
		{
			printf("Nome invalido.\n");
			ok = false;
			continue;
		}
	}
	while (!ok);

	exit = true;
}

bool isValidRoomName(const char *buf)
{
	int len = strlen(buf);
	if (len == 0 || len > 64)
		return false;
	for (__typeof__(rooms.begin()) it = rooms.begin(); it != rooms.end(); ++it)
		if (strcmp(it->second.c_str(), buf) == 0)
			return false;
	return true;
}

void doCreateRoomOperation(bool &exit)
{
	char buf[256];
	bool ok;
	do
	{
		printf("Nome: ");
		scanf(" ");
		fgets(buf, 256, stdin);
		for (int i = strlen(buf)-1; buf[i] == ' ' || buf[i] == '\n' || buf[i] == '\t' || buf[i] == '\r'; --i)
			buf[i] = 0;
		ok = true;
		if (!isValidRoomName(buf))
		{
			printf("Nome invalido.\n");
			ok = false;
			continue;
		}
	}	
	while (!ok);
	
	// Conexao com o coordenador
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	
	sockaddr_in serv_addr;
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	inet_pton(AF_INET, servIp, &serv_addr.sin_addr);
	serv_addr.sin_port = htons(COORDINATOR_PORT);
	
	if (connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
		eprintf("Erro ao conectar ao coordenador\n");
	
	// Envia mensagem de criacao de sala.
	{
		MessageLCCreateRoom *msg = new MessageLCCreateRoom;
		msg->type = LC_CREATE_ROOM;
		strcpy(msg->room_name, buf);
		sendMessageLC(sockfd, (MessageLC*)msg);
	}
	
	// Recebe mensagem de OK.
	{
		MessageCL *rawmsg = recvMessageCL(sockfd);
		if (rawmsg == 0 || rawmsg->type != CL_ROOM_CREATED)
			eprintf("Erro de comunicacao ao criar sala");
	}
}

void doDeleteRoomOperation(bool &exit)
{
	int port;
	bool ok;
	do
	{
		printf("Porta: ");
		ok = true;
		if (scanf(" %d", &port) != 1)
		{
			printf("Porta invalida.\n");
			discardInputBuffer();
			ok = false;
			continue;
		}
		if (port < 1000 || port > 65535)
		{
			printf("Porta invalida.\n");
			ok = false;
			continue;
		}
	}	
	while (!ok);
	
	// Conexao com o coordenador.
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	
	sockaddr_in serv_addr;
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	inet_pton(AF_INET, servIp, &serv_addr.sin_addr);
	serv_addr.sin_port = htons(COORDINATOR_PORT);
	
	if (connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
		eprintf("Erro ao conectar ao coordenador\n");
	
	// Envia mensagem de delecao de sala.
	{
		MessageLCDeleteRoom *msg = new MessageLCDeleteRoom;
		msg->type = LC_DELETE_ROOM;
		msg->room_port = port;
		sendMessageLC(sockfd, (MessageLC*)msg);
	}
	
	// Recebe mensagem de OK.
	{
		MessageCL *rawmsg = recvMessageCL(sockfd);
		if (rawmsg == 0 || rawmsg->type != CL_ROOM_DELETED && rawmsg->type != CL_ROOM_HAS_ASSOCIATES)
			eprintf("Erro de comunicacao ao deletar sala");
	}
}

void chatLoop()
{
	// Conexao com o servidor.
	int chatfd = socket(AF_INET, SOCK_STREAM, 0);
	
	sockaddr_in serv_addr;
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	inet_pton(AF_INET, servIp, &serv_addr.sin_addr);
	serv_addr.sin_port = htons(roomToEnter);
	
	if (connect(chatfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
		eprintf("Erro ao conectar ao servidor da sala\n");
	
	// Envio de mensagem de associacao a sala.
	{
		MessageLSAssociate *msg = new MessageLSAssociate;
		msg->type = LS_ASSOCIATE;
		strcpy(msg->user_name, username);
		sendMessageLS(chatfd, (MessageLS*)msg);
		delete msg;
	}
	
	// Recebimento de mensagem de OK.
	{	
		MessageSL *msg = recvMessageSL(chatfd);
		if (msg == 0 || msg->type != SL_ASSOCIATE_OK)
			eprintf("Erro ao entrar na sala.");
		delete msg;
	}
		
	roomExit = false;
		
	// Inicializa tela de chat.
	initscr();
	cbreak();

	composing[0] = 0;
	messages.clear();
	render();
	dispatchScreen();
	
	while (!roomExit)
	{
		// Monitorando 0 (stdin) e chatfd (socket do servidor)
		fd_set fds;
		FD_ZERO(&fds);
		FD_SET(0, &fds); // stdin
		FD_SET(chatfd, &fds);
		int n = std::max(0, chatfd) + 1;

		// Interpreta comando de teclado ou pacote novo.
		int fd = select(n, &fds, NULL, NULL, NULL);
		if (fd == -1 || fd == 0)
			eprintf("Erro ao aguardar select.\n");
		else if (FD_ISSET(0, &fds))
			getKeyboardInput(chatfd);
		else if (FD_ISSET(chatfd, &fds))
			getPacket(chatfd);
		else
			eprintf("Condicao absurda ao aguardar select.\n");
	}
	
	// Envia mensagem de desassociacao.
	{
		MessageLS *msg = new MessageLS;
		msg->type = LS_DEASSOCIATE;
		sendMessageLS(chatfd, msg);
		delete msg;
	}
	
	// Recebe mensagem de OK.
	{
		MessageSL *msg = recvMessageSL(chatfd);
		if (msg != 0)
			delete msg;
	}
	
	endwin();
	
	printf("Saiu da sala. Motivo: %s\n", roomExitReason.c_str());
}

void getKeyboardInput(int chatfd)
{
	int ch = getch();
	if (ch == 27) // ESC
	{
		roomExit = true;
		roomExitReason = "Pedido do usuario.";
	}
	else if (ch == 10) // ENTER
	{
		MessageLSMessage *msg = new MessageLSMessage;
		msg->type = LS_MESSAGE;
		strcpy(msg->message, composing);
		if (sendMessageLS(chatfd, (MessageLS*)msg) == -1)
		{
			roomExit = true;
			roomExitReason = "Falha na conexao com o servidor.";
			return;
		}
		delete msg;
		
		composing[0] = 0;
		render();
		dispatchScreen();
	}
	else
	{
		if (ch == 127)
		{
			composing[strlen(composing) - 1] = 0;
		}
		else
		{
			char s[2] = {ch};
			strcat(composing, s);
		}
		render();
		dispatchScreen();
	}
}

void render()
{
	memset(charbuffer, 0, sizeof(charbuffer));
	for (int i=0; i<LINES; ++i)
		memset(charbuffer[i], ' ', sizeof(charbuffer[i]));
	int r=0, c=0;
	int len = strlen(composing);
	for (int i=0; i<len; ++i)
	{
		charbuffer[r][c] = composing[i];
		++c;
		if (c == COLS)
			++r, c = 0;
	}
	curr = r; curc = c;
	++r, c = 0;
	for (int i=0; i<COLS; ++i)
		charbuffer[r][i] = '-';
	++r;
	bool goon = true;
	for (__typeof__(messages.rbegin()) it = messages.rbegin(); goon && it != messages.rend(); ++it)
	{
		for (int i=0; i<(int)it->length(); ++i)
		{
			charbuffer[r][c] = (*it)[i];
			++c;
			if (c == COLS)
				++r, c = 0;
			if (r == LINES)
			{
				goon = false;
				break;
			}
		}
		if (c != 0)
			++r, c = 0;
		if (r == LINES)
			goon = false;
	}
}

void dispatchScreen()
{
	for (int i=0; i<LINES; ++i)
		mvprintw(i, 0, "%s", charbuffer[i]);
	move(curr, curc);
	refresh();
	move(curr, curc);
}

void getPacket(int chatfd)
{
	MessageSL* rawmsg = recvMessageSL(chatfd);
	if (rawmsg == 0)
	{
		roomExit = true;
		roomExitReason = "Falha na conexao com o servidor.";
		return;
	}
	if (rawmsg->type == SL_MESSAGE)
	{
		MessageSLMessage *msg = (MessageSLMessage*)rawmsg;
		messages.push_back(msg->message);
		while (messages.size() > 100)
			messages.pop_front();
		render();
		dispatchScreen();
	}
	else
		eprintf("Mensagem inesperada do servidor.\n");
}

void discardInputBuffer()
{
	fd_set fds;
	while (true)
	{
		FD_ZERO(&fds);
		FD_SET(0, &fds);
		timeval t = (timeval) {0, 0};
		int k;
		if ((k=select(1, &fds, NULL, NULL, &t)) <= 0)
		{
			break;
		}
	}
	char buf[1000];
	fgets(buf, 1000, stdin);
}
